#    pythonequations is a collection of equations expressed as Python classes
#    Copyright (C) 2008 James R. Phillips
#    2548 Vera Cruz Drive
#    Birmingham, AL 35235 USA
#    email: zunzun@zunzun.com
#
#    License: BSD-style (see LICENSE.txt in main source directory)
#    Version info: $Id: UserDefinedFunction.py 317 2011-07-21 20:43:59Z zunzun.com $

import pythonequations, pythonequations.EquationBaseClasses, pythonequations.ExtraCodeForEquationBaseClasses
import numpy # implicitly required by compiling the userFunctionCodeObject in the method EvaluateCachedData() below
numpy.seterr(all = 'raise') # numpy raises warnings, convert to exceptions to trap them
import StringIO, parser


class UserDefinedFunction3D(pythonequations.EquationBaseClasses.Equation3D):
    RequiresAutoGeneratedGrowthAndDecayForms = False
    RequiresAutoGeneratedOffsetForm = False
    RequiresAutoGeneratedReciprocalForm = False
    RequiresAutoGeneratedInverseForms = False
    _name ="User Defined Function"
    _HTML = "z = User Defined Function"
    function_cpp_code = ';' # unused
    
    userDefinedFunctionFlag = True
    userDefinedFunctionText = ''
        
    
    def EvaluateCachedData(self, coeff, _id):

        self.safe_dict['X'] = _id[0]
        self.safe_dict['Y'] = _id[1]

        # define coefficient values before calling eval
        for i in range(len(self.coefficientDesignatorTuple)):
            self.safe_dict[self.coefficientDesignatorTuple[i]] = coeff[i]
        
        # eval uses previously compiled code for improved performance
        # based on http://lybniz2.sourceforge.net/safeeval._HTML
        try:
            return eval(self.userFunctionCodeObject, {"__builtins__":None, 'numpy':numpy}, self.safe_dict)
        except:
            return numpy.ones(len(_id[0])) * 1.0E300


    def CreateCacheGenerationList(self):
        self.CacheGenerationList = []
        self.CacheGenerationList.append([pythonequations.ExtraCodeForEquationBaseClasses.CG_X(NameOrValueFlag=1), []])
        self.CacheGenerationList.append([pythonequations.ExtraCodeForEquationBaseClasses.CG_Y(NameOrValueFlag=1), []])


    def CodePYTHON(self):
        s  = """# To the best of my knowledge this code is correct.
# If you find any errors or problems please contact
# me at zunzun@zunzun.com.
#      James


from numpy import *

"""
        s += "# " + self.FittingTargetDict[self.fittingTarget][1] + "\n\n"
        s += "def " + self.__class__.__name__ + "_model(x_in, y_in):\n"

        s += "\t# coefficients\n"
        for i in range(len(self.coefficientArray)):
            s += "\t" + self.coefficientDesignatorTuple[i] + " = %-.16E" % (self.coefficientArray[i]) + "\n"
        s += "\n"

        s += "\tX = x_in\n"
        s += "\tY = y_in\n"
        s += "\ttemp = eval('''" + self.userDefinedFunctionText + "''')\n"

        s += "\treturn temp\n"
        return s


    def CodeCPP(self):
        raise NotImplementedError, 'Not implemented for user defined functions'


    def CodeJAVA(self):
        raise NotImplementedError, 'Not implemented for user defined functions'


    def CodeCS(self):
        raise NotImplementedError, 'Not implemented for user defined functions'


    def CodeSCILAB(self):
        raise NotImplementedError, 'Not implemented for user defined functions'


    def CodeMATLAB(self):
        raise NotImplementedError, 'Not implemented for user defined functions'


    def CodeVBA(self):
        raise NotImplementedError, 'Not implemented for user defined functions'


    def Initialize(self):
        self.additionalDesignatorList = []

        if self.userDefinedFunctionText == '':
            self._HTML = "z = User Defined Function"
            return

        pythonequations.EquationBaseClasses.Equation3D.Initialize(self)


    def ParseVerifyCreateSafeDictAndCompileUserFunctionString(self):
        
        # shift user functions into numpy namespace at run time, not import time
        numpySafeTokenList = []
        for key in self.functionDict.keys():
            numpySafeTokenList += self.functionDict[key]
        for key in self.constantsDict.keys():
            numpySafeTokenList += self.constantsDict[key]
            
        # to shift user functions such as "power" into the numpy namespace "numpy.power" for evaluation
        for token in numpySafeTokenList:
            exec(token + ' = numpy.' + token)
        
        # no blank lines of text, StringIO.StringIO() allows using file methods on text
        stringToConvert = ''
        rawData = StringIO.StringIO(self.userDefinedFunctionText).readlines()
        
        for line in rawData:
            stripped = line.strip()
            if len(stripped) > 0: # no empty strings
                if stripped[0] != '#': # no comment-only lines
                    stringToConvert += stripped + '\n'

        # convert brackets to parentheses
        stringToConvert = stringToConvert.replace('[', '(').replace(']', ')')
        
        if stringToConvert == '':
            raise Exception('You must enter some function text for the software to use.')

        if -1 != stringToConvert.find('='):
            raise Exception('Please do not use an equals sign "=" in your text.')

        st = parser.expr(stringToConvert)
        tup = st.totuple()
        tokens = self.GetTokensFromTuple(tup) # inherited method

        if '^' in tokens:
            raise Exception('The caret symbol "^" is not recognized by the parser, please substitute double asterisks "**" for "^".')
            
        if 'ln' in tokens:
            raise Exception("The parser uses log() for the natural log function, not ln(). Please use log() in your text.")

        if 'abs' in tokens:
            raise Exception("The parser uses fabs() for the absolute value, not abs(). Please use fabs() in your text.")

        if 'EXP' in tokens:
            raise Exception("The parser uses lower case exp(), not upper case EXP(). Please use lower case exp() in your text.")

        if 'LOG' in tokens:
            raise Exception("The parser uses lower case log(), not upper case LOG(). Please use lower case log() in your text.")

        # test for required reserved tokens
        tokenNames = list(set(tokens) - set(numpySafeTokenList))
        if 'X' not in tokenNames:
            raise Exception('You must use a separate upper case "X" in your function to enter a valid function of X.')
        if self.dimensionality == 3:
            if 'Y' not in tokenNames:
                raise Exception('You must use a separate upper case "Y" in your function to enter a valid function of Y.')

        if self.dimensionality == 2:
            self.coefficientDesignatorTuple = sorted(list(set(tokenNames) - set(['X'])))
        else:
            self.coefficientDesignatorTuple = sorted(list(set(tokenNames) - set(['X', 'Y'])))
                
        if len(self.coefficientDesignatorTuple) == 0:
            raise Exception('I could not find any equation parameter or coefficient names, please check the function text')

        # now compile code object using safe tokens
        self.safe_dict = dict([ (k, locals().get(k, None)) for k in numpySafeTokenList ])
            
        # later evals re-use this compiled code for improved performance in EvaluateCachedData() methods
        self.userFunctionCodeObject = compile(stringToConvert, '<string>', 'eval')
